
#include "Particle.hpp"

void ParticleGestionner::attachParticleEmitter(ParticleEmitter* pe_) {
	list<ParticleEmitter*>::iterator begin_ = m_particleEmitterList.begin();
	pe_->setIterator(m_particleEmitterList.insert(begin_, pe_));
}

list<ParticleEmitter*>::iterator ParticleGestionner::detachParticleEmitter(ParticleEmitter* pe_) {
	list<ParticleEmitter*>::iterator it_ = m_particleEmitterList.erase(pe_->getIterator());
	delete pe_;
	return it_;
}


ParticleEmitter::ParticleEmitter(float pointSize_, float sizeUpdate_, float alphaDecrease_, 
							 Material* material_, float intensity_, float tet_, int limits_, int pack_) :
	m_direction(glm::vec3(0.0f,0.0f,0.0f)), m_time(0.0f), m_accTime(0.0f), 
	m_pointSize(pointSize_), m_sizeUpdate(sizeUpdate_), m_alphaDecrease(alphaDecrease_),
	m_intensity(intensity_), m_totalExistenceTime(tet_),
	m_limits(limits_), pack(pack_), m_material(material_) 
{
	m_buffers = new GLuint[3]; // create here for deletion
	m_lastLaunched = 0;
	m_position = glm::vec3(0.0f,0.0f,0.0f);
	glPointSize(5.0f);
	//createBuffer(); // has to be called before any call to render() or update()
};



bool ParticleEmitter::update(float time) {
	// is the total time elapsed ?
	if (m_totalExistenceTime < 0.0) return false;
 	// updating particle launch
	m_totalExistenceTime -= time;
	m_time += time;
	m_accTime += time;
	int tmp = m_time * pack;
	if (tmp > 0) {
		m_time = 0.0;
		// limits -= tmp;
		m_lastLaunched += tmp;
		//if (limits < 0) return false;
	}
	if (m_lastLaunched > m_limits) {
		m_lastLaunched = m_limits;
		//cout << "overflow" << endl;
	};  
	return true; 
}


void ParticleEmitter::render() {
	glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
	glTexEnvf( GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_TRUE );
	glLoadIdentity();
	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glPointSize(20.0f);
	glEnable(GL_POINT_SPRITE_ARB);
	m_material->use();
	glm::vec3 camPosition = m_camera->getPosition();

	// uniform attribution
	glUniform1f(m_globalTime_id, m_accTime);
	glUniform1f(m_alphaDecrease_id, m_alphaDecrease);
	glUniform3f(m_initialPosition_id, m_position.x,m_position.y,m_position.z);
	glUniform3f(m_cameraPosition_id, camPosition.x,camPosition.y,camPosition.z);  
	glUniform1f(m_initialPointSize_id, m_pointSize);
	glUniform1f(m_sizeUpdate_id, m_sizeUpdate);
	GLfloat projection[16];
	glGetFloatv(GL_PROJECTION_MATRIX, projection);
	glUniformMatrix4fv(m_MVPMatrix_id,1,GL_FALSE,projection);

	// buffer binding with attrib array pointer
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[START_TIME_ARRAY]);
	glVertexAttribPointer(START_TIME_ATTR, 1, GL_FLOAT, GL_FALSE, 0, 0);
  
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[INITIAL_SPEED_ARRAY]);
	glVertexAttribPointer(INITIAL_SPEED_ATTR,1,GL_FLOAT,GL_FALSE,0,0);
	//cout << "direction buffer sending ..." << endl;

	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[DIRECTION_ARRAY]);
	glVertexAttribPointer(DIRECTION_ATTR,3,GL_FLOAT,GL_FALSE,0,0);
	//glVertexAttrib3fv(DIRECTION_ATTR, direction);

	glEnableVertexAttribArray(START_TIME_ATTR);
	glEnableVertexAttribArray(INITIAL_SPEED_ATTR);
	glEnableVertexAttribArray(DIRECTION_ATTR);
#if 0
	{
		int length,size;
		GLenum type;
		char* name = new char[100];
		glGetActiveAttrib(	m_material->getShaderProgram()->handle(),
						INITIAL_SPEED_ATTR,
						100,
						&length,
						&size,
						&type,
						name);
		cout << "DEBUG INFO : active attribute with index INITIAL_SPEED_ATTR " << INITIAL_SPEED_ATTR << " has been loaded in a buffer with length " << length << "." << endl
			<< "		attribute size is " << size << " and type is " << type << " " << GL_FLOAT << endl
			<< "		name is " << name << endl;
		delete name;
	};
#endif
	{
		int length,size;
		GLenum type;
		char* name = new char[100];
		glGetActiveAttrib(	m_material->getShaderProgram()->handle(),
						DIRECTION_ATTR,
						100,
						&length,
						&size,
						&type,
						name);
		cout << "shader program index is " << m_material->getShaderProgram()->handle() << endl;
		cout << "DEBUG INFO : active attribute with index DIRECTION_ATTR " << DIRECTION_ATTR << " has been loaded in a buffer with length " << length << "." << endl
			<< "		attribute size is " << size << " and type is " << type << " | vec3 " << GL_FLOAT_VEC3 << " | GL_FLOAT :  " << GL_FLOAT << endl
			<< "		name is " << name << endl;
		delete name;
	}
	cout << "GL_MAX_VERTEX_ATTRIBS is " << GL_MAX_VERTEX_ATTRIBS << endl;

	// drawing
	//cout << "lastLaunched " << m_lastLaunched << endl;
	glDrawArrays(GL_POINTS,0,m_lastLaunched);


	m_material->unuse();
	glDisable(GL_POINT_SPRITE_ARB);
	glDisable(GL_BLEND);
	glDisable(GL_VERTEX_PROGRAM_POINT_SIZE);

	glDisableVertexAttribArray(START_TIME_ATTR);
	glDisableVertexAttribArray(INITIAL_SPEED_ATTR);
	glDisableVertexAttribArray(DIRECTION_ATTR);
}


void ParticleEmitter::createBuffer() {
	// attrib location
	glBindAttribLocation(m_material->getShaderProgram()->handle(),START_TIME_ATTR,"startTime");
	glBindAttribLocation(m_material->getShaderProgram()->handle(),INITIAL_SPEED_ATTR,"initialSpeed");
	glBindAttribLocation(m_material->getShaderProgram()->handle(),DIRECTION_ATTR,"PPDirection");

	glLinkProgram(m_material->getShaderProgram()->handle());
	glValidateProgram(m_material->getShaderProgram()->handle());

	// uniform location
	m_globalTime_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"globalTime");
	m_initialPosition_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"initialPosition");
	m_cameraPosition_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"cameraPosition");
	m_alphaDecrease_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"alphaDecrease");
	m_sizeUpdate_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"sizeUpdate");
	m_initialPointSize_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"initialPointSize");
	m_MVPMatrix_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"MVPMatrix");

	direction = new GLfloat[3 * m_limits];
	initialSpeed = new GLfloat[m_limits];
	startTime = new GLfloat[m_limits];
	//cout << "particle direction" << endl;
	for (int i = 0; i < m_limits; i++) {
		// direction declaration
		const int amplitude = 1000;
		const int amplitude_h = 500; // half
		float amplitude_f = amplitude;
		glm::vec3 tmp;
		tmp.x = ((rand() % amplitude) - amplitude_h) / amplitude_f;
		tmp.y = ((rand() % amplitude) - amplitude_h) / amplitude_f;
		tmp.z = ((rand() % amplitude) - amplitude_h) / amplitude_f;

		//glm::vec3 tmp(((rand() % 1000) - 500) / 1000.0,((rand() % 1000) - 500) / 1000.0,((rand() % 1000) - 500) / 1000.0);
		tmp = (m_direction + 0.5f * tmp);
		direction[3*i]   = tmp.x;
		direction[3*i+1] = tmp.y;
		direction[3*i+2] = tmp.z;
		//cout << tmp.x << " " << tmp.y << " " << tmp.z << endl;
		// initial speed declaration
		float accTime = startTime[i] = i / ((float) pack);
		initialSpeed[i] = exp(- 20.0 * (accTime)) * m_speedIntensity * ( ((rand() %1000) - 500) / 2000.0 + 1.0 );
	}
	// buffer filling
	glGenBuffers(3,m_buffers);
	// direction buffer
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[DIRECTION_ARRAY]);
	glBufferData(GL_ARRAY_BUFFER,3*sizeof(GLfloat)*m_limits,direction,GL_STATIC_DRAW);
	// speed buffer
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[INITIAL_SPEED_ARRAY]);
	glBufferData(GL_ARRAY_BUFFER,sizeof(GLfloat) * m_limits,initialSpeed,GL_STATIC_DRAW);
	// start time array
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[START_TIME_ARRAY]);
	glBufferData(GL_ARRAY_BUFFER,sizeof(GLfloat) * m_limits,startTime,GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER,0);
};

/** initializing static membres of ParticleGestionner */
list<ParticleEmitter*> ParticleGestionner::m_particleEmitterList;

map<string, ParticleSetting*> ParticleGestionner::m_particleSetting;


ParticleEmitter* ParticleGestionner::createParticleEmitter(string particleName) {
	map<string, ParticleSetting*>::iterator it;
	it = m_particleSetting.find(particleName);
	if (it == m_particleSetting.end()) {
		cout << "ERROR : Particle Gestionner was not able to located particle settings called : " << particleName << "." << endl;
		return NULL;
	} else {
		ParticleSetting* tmp = m_particleSetting[particleName];
		if (tmp->m_particleSubType == "default") {
			ParticleEmitter* new_p = new ParticleEmitter(tmp->m_pointSize, 
														 tmp->m_sizeUpdate,
														 tmp->m_alphaDecrease,
														 tmp->m_material,
														 tmp->m_intensity,
														 tmp->m_totalExistenceTime,
														 tmp->m_limits,
														 tmp->m_pack);
			// registering new_p to this particle gestionner
			attachParticleEmitter(new_p);
			return new_p;
		} else if (tmp->m_particleSubType == "projectile") {
			ParticleProjectileEmitter* new_p = new ParticleProjectileEmitter(tmp->m_pointSize, 
														 tmp->m_sizeUpdate,
														 tmp->m_alphaDecrease,
														 tmp->m_material,
														 tmp->m_intensity,
														 tmp->m_totalExistenceTime,
														 tmp->m_limits,
														 tmp->m_pack);
			// registering new_p to this particle gestionner
			attachParticleEmitter(new_p);
			return new_p;
		}; 
	}
	return NULL; // default case, must not happen
}

void ParticleGestionner::loadParticleScript(string filename_) {
	TiXmlDocument doc("data/particle.xml");//filename_.c_str());
	if (!doc.LoadFile()) cout << "ERROR : impossible to load particle description xml file." << endl;

    TiXmlHandle hDoc(&doc);
    TiXmlElement* p_element;
    TiXmlHandle hRoot(0);

    p_element = hDoc.FirstChildElement().Element();//"ParticleSystem");//.Element();
    for (p_element; p_element; p_element = p_element->NextSiblingElement()) {
		ParticleSetting* settings = new ParticleSetting();
		settings->m_particleSubType = "default";

        string name;
        (*p_element).QueryStringAttribute("name", &name);
		(*p_element).QueryStringAttribute("subtype", &(settings->m_particleSubType));

		m_particleSetting[name] = settings;


        hRoot = TiXmlHandle(p_element);
        // searching arguments
        TiXmlElement* p_arg;
        p_arg = hRoot.FirstChildElement().Element();
        for (/*p_arg*/; p_arg; p_arg = p_arg->NextSiblingElement()) {
            string nodeName = (*p_arg).ValueStr();
            //cout << tab << "parameter name " << nodeName << endl;
			if (nodeName == "PointSize") {
				(*p_arg).QueryFloatAttribute("value",&(settings->m_pointSize));
			} else if (nodeName == "SizeUpdate") {
				(*p_arg).QueryFloatAttribute("value",&(settings->m_sizeUpdate));
			} else if (nodeName == "AlphaDecrease") {
				(*p_arg).QueryFloatAttribute("value",&(settings->m_alphaDecrease));
			} else if (nodeName == "Limits") {
				(*p_arg).QueryIntAttribute("value",&(settings->m_limits));
			} else if (nodeName == "Pack") {
				(*p_arg).QueryIntAttribute("value",&(settings->m_pack));
			} else if (nodeName == "Intensity") {
				(*p_arg).QueryFloatAttribute("value",&(settings->m_intensity));
			} else if (nodeName == "TotalExistenceTime") {
				(*p_arg).QueryFloatAttribute("value",&(settings->m_totalExistenceTime));
			} else if (nodeName == "Material") {
				string materialName, textureName, fragmentShader, vertexShader;
				(*p_arg).QueryStringAttribute("name", &materialName);
				(*p_arg).QueryStringAttribute("textureName", &textureName);
				(*p_arg).QueryStringAttribute("fragmentShader", &fragmentShader);
				(*p_arg).QueryStringAttribute("vertexShader", &vertexShader);
				settings->m_material = MaterialGestionner::addTexturedMaterial(materialName, textureName, vertexShader, fragmentShader);
			};
        };
    }

}

void ParticleGestionner::update(float time) {
	list<ParticleEmitter*>::iterator it = m_particleEmitterList.begin();
	for (; it != m_particleEmitterList.end();) {
		if (!(*it)->update(time)) {
			it = detachParticleEmitter(*it);
		} else it++;
	}
}

void ParticleGestionner::render() {
	//cout << "# particles : " << m_particleEmitterList.size() << endl;
	list<ParticleEmitter*>::iterator it = m_particleEmitterList.begin();
	for (; it != m_particleEmitterList.end();) {
		(*it)->render();
		it++;
	}
}
